package org.smartboot.compare.array;

import org.smartboot.compare.Accessor;
import org.smartboot.compare.ComparatorContext;
import org.smartboot.compare.ComparatorRegister;
import org.smartboot.compare.FieldCache;
import org.smartboot.compare.Option;
import org.smartboot.compare.Path;
import org.smartboot.compare.comparator.Comparator;
import org.smartboot.compare.comparator.DispatcherComparator;
import org.smartboot.compare.constants.CompareConstants;
import org.smartboot.compare.difference.BaseDifference;
import org.smartboot.compare.difference.ComplementSetDifference;
import org.smartboot.compare.difference.Difference;
import org.smartboot.compare.difference.DifferenceGroup;
import org.smartboot.compare.difference.NullOfOneObject;
import org.smartboot.compare.difference.SizeDifference;
import org.smartboot.compare.utils.ComparatorUtils;

import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.Set;

/**
 * @author qinluo
 * @date 2024-03-24 02:18:56
 * @since 1.0.7
 */
@SuppressWarnings({"unchecked", "rawtypes"})
public class ArrayHelper {

    public static int getLength(Object array) {
        return array == null ? 0 : Array.getLength(array);
    }

    public static Difference compare(Object expect, Object actual, ComparatorContext ctx, int type, Class<?> elementType) {
        ArrayAccessor expectAccessor = new ArrayAccessor(expect, type, elementType);
        ArrayAccessor actualAccessor = new ArrayAccessor(actual, type, elementType);
        return compare(expectAccessor, actualAccessor, ctx);
    }

    public static Difference compare(Accessor expectAccessor, Accessor actualAccessor, ComparatorContext ctx) {
        int expectSize = expectAccessor.getLength();
        int actualSize = actualAccessor.getLength();

        assert actualAccessor.getSourceType() == expectAccessor.getSourceType();

        // LOOSE_MODE, new int[0] == null;
        if (expectSize == actualSize && expectSize == 0 && ctx.hasOption(Option.LOOSE_MODE)) {
            return Difference.SAME;
        }

        // Transfer-As-Set feature.
        if (expectAccessor.supportOption(Option.TRANS_AS_SET) && actualAccessor.supportOption(Option.TRANS_AS_SET)
                && CompareConstants.limitless(Math.max(expectSize, actualSize))
                && ctx.hasOption(Option.TRANS_AS_SET)) {
            return compareAsSet(expectAccessor, actualAccessor, ctx);
        }

        // Null of one.
        if (expectAccessor.getSource() == null || actualAccessor.getSource() == null) {
            return new NullOfOneObject(ctx.getPath(), expectAccessor.getSource(), actualAccessor.getSource());
        }
        // Empty array
        else if (expectSize == actualSize && expectSize == 0) {
            return Difference.SAME;
        }

        DifferenceGroup group = DifferenceGroup.of();

        // Size not equals
        if (expectSize != actualSize) {
            group.addDifference(new SizeDifference(ctx.getPath(), expectSize, actualSize, expectAccessor.getSource(), actualAccessor.getSource()));
        }

        // Check has IMMEDIATELY_INTERRUPT configured.
        if (group.hasDifferences() && ctx.hasOption(Option.IMMEDIATELY_INTERRUPT)) {
            return group;
        }

        // Compare minimum size.
        int min = Math.min(expectSize, actualSize);

        // Given a chance to sort array/list before compare.
        ctx.getFeatureFunction().sort(ctx, expectAccessor.getSource(),
                actualAccessor.getSource(), expectAccessor.getSourceType());

        for (int index = 0; index < min; index++) {
            Object newExpect = expectAccessor.elementAt(index);
            Object newActual = actualAccessor.elementAt(index);

            if (ComparatorUtils.checkAllNull(newActual, newExpect)) {
                continue;
            }

            // Create new path and check filters
            Path newPath = ctx.createIndexPath(index);
            if (ctx.getFilters().filtered(new FieldCache(index + "", Object.class), ctx, newPath)) {
                ctx.addSkippedField(newPath.getFullPath());
                continue;
            }

            if (expectAccessor.getSourceType() <= CompareConstants.WRAPPER) {
                Class<?> rawType = expectAccessor.elementType();
                Comparator found = ComparatorRegister.findComparator(rawType);
                if(found != null && !(found instanceof DispatcherComparator)) {
                    ComparatorContext<Object> nc = ctx.clone(newExpect, newActual);
                    nc.incr();
                    nc.setPath(newPath);
                    group.addDifference(found.compare(nc));
                } else {
                    group.addDifference(Comparator.callDefaultEquals(newExpect, newActual, newPath));
                }

            } else {
                // Dispatch compare request to dispatcher.
                ComparatorContext<Object> nc = ctx.clone(newExpect, newActual);
                nc.incr();
                nc.setPath(newPath);
                group.addDifference(ComparatorRegister.dispatcherComparator().compare(nc));
            }

            // Interrupted case.
            if (group.hasDifferences() && ctx.hasOption(Option.IMMEDIATELY_INTERRUPT)) {
                break;
            }

            // Special process for array.
            if (expectAccessor.supportOption(Option.BEAUTIFUL_ARRAY_RESULT)
                    && group.hasDifferences()
                    && ctx.hasOption(Option.IMMEDIATELY_INTERRUPT)) {
                break;
            }
        }

        // Optimize array compare result.
        if (group.hasDifferences()
                && expectAccessor.supportOption(Option.BEAUTIFUL_ARRAY_RESULT)
                && ctx.hasOption(Option.BEAUTIFUL_ARRAY_RESULT)) {
            return new BaseDifference(ctx.getPath(), expectAccessor.getSource(), actualAccessor.getSource());
        }

        for (int index = min; index < expectSize; index++) {
            Object value = expectAccessor.elementAt(index);
            Path path = ctx.createIndexPath(index);
            group.addDifference(new BaseDifference(path, value, null));
        }

        for (int index = min; index < actualSize; index++) {
            Object value = actualAccessor.elementAt(index);
            Path path = ctx.createIndexPath(index);
            group.addDifference(new BaseDifference(path, null, value));
        }

        return group;
    }

    private static Difference compareAsSet(Accessor expectAccessor, Accessor actualAccessor, ComparatorContext ctx) {
        Set<Object> set1 = expectAccessor.convert2Set();
        Set<Object> set2 = actualAccessor.convert2Set();

        // 1,2,3,6
        // 2,3,4,7
        Iterator<Object> iterator = set1.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            if (set2.contains(next)) {
                set2.remove(next);
                iterator.remove();
            }
        }

        if (set1.isEmpty() && set2.isEmpty()) {
            return null;
        }

        return new ComplementSetDifference(ctx.getPath(), set2, set1);
    }
}
